# from src.utils.markdown_report import MdReport
import pandas as pd
import numpy as np
from tqdm import tqdm
from conf import config
from src.utils.logger import get_logger
import pickle
logger = get_logger('数据分箱')
# md = MdReport('所有特征分箱.md', title='所有非空特征分箱报告')
code2name = pickle.load(open(config.code2name, 'rb'))


class Binning:
    def __init__(self, df, target, feas=[]):
        self.df = df  # dataframe的数据类型
        self.target = target  # 目标变量
        self.q = 10  # 数值型分箱分割的箱数
        if feas == []:  # 可以指定特征
            all_cols = df.columns.tolist()
            all_cols.remove(target)
            self.feas = all_cols
        else:
            self.feas = feas
        self.results = None

    def _get_bin_target_dist(self, bin_val_cnt, bin_cnt):
        if len(bin_val_cnt) == 0:
            return 0, 0, 0, 0

        try:
            bin_good_cnt = bin_val_cnt[0]
            bin_good_rate = round(bin_good_cnt/bin_cnt, 5)
            if bin_good_rate == 1.0:
                bin_bad_rate = 0.0
                bin_bad_cnt = 0
            else:
                bin_bad_cnt = bin_val_cnt[1]
                bin_bad_rate = round(bin_bad_cnt/bin_cnt, 5)
        except:
            bin_bad_cnt = bin_val_cnt[1]
            bin_bad_rate = round(bin_bad_cnt/bin_cnt, 5)
            if bin_bad_rate == 1.0:
                bin_good_rate = 0.0
                bin_good_cnt = 0
            else:
                bin_good_cnt = bin_val_cnt[0]
                bin_good_rate = round(bin_good_cnt/bin_cnt, 5)

        return bin_bad_cnt, bin_bad_rate, bin_good_cnt, bin_good_rate

    def _cat_num_spliter(self):
        df, feas = self.df, self.feas
        all_missing_cols = []
        for col in feas:
            if df[col].isna().sum() / len(df) == 1:
                all_missing_cols.append(col)
        for col in all_missing_cols:
            logger.info('删除以全空列%s: %s' %
                        (col, code2name.get(col.upper(), 'N/A')))
        feas = list(set(feas)-set(all_missing_cols))
        cat_cols = []
        num_cols = []
        for col in feas:
            if df[col].dtype == object:
                cat_cols.append(col)
            else:
                if df[col].value_counts().__len__() < 10:  # 0.01*len(df) 当数值型特征的值过少时，按类别型特征处理
                    cat_cols.append(col)
                else:
                    num_cols.append(col)
        return cat_cols, num_cols

    def _cat_binning(self, cat_feas):
        logger.info('正在处理分类型特征...')
        df = self.df
        meta_feas = []
        for fea in tqdm(cat_feas[:]):
            # logger.info(fea)
            val_cnt = df[fea].value_counts(dropna=False)
            if val_cnt.__len__() > 0.8 * len(df):
                meta_feas.append(fea)
                logger.info('%s可能为meta类型特征，不进行探查' % fea)
                pass
            else:
                bins = val_cnt.index.tolist()
                bins = ['N/A' if pd.isnull(i) else i for i in bins]
                # bin = ['N/A' if np.isnan(i) else i for i in bin]
                cnt = val_cnt.values.tolist()

                val_cnt_df = pd.DataFrame({'bin': bins, 'cnt': cnt})
                bad_rate = []
                good_rate = []
                bad_cnt = []
                good_cnt = []
                for bin in bins:
                    if bin == 'N/A':
                        bin_val_cnt = df[self.target][df[fea].isnull()
                                                      ].value_counts()

                        bin_cnt = df[fea].isnull().sum()

                    else:
                        bin_val_cnt = df[self.target][df[fea]
                                                      == bin].value_counts()

                        bin_cnt = sum(bin_val_cnt)
                    bin_bad_cnt, bin_bad_rate, bin_good_cnt, bin_good_rate = self._get_bin_target_dist(
                        bin_val_cnt, bin_cnt)
                    bad_cnt.append(bin_bad_cnt)
                    bad_rate.append(bin_bad_rate)
                    good_cnt.append(bin_good_cnt)
                    good_rate.append(bin_good_rate)

                val_cnt_df['bad_cnt'] = bad_cnt
                val_cnt_df['bad_rate'] = bad_rate
                val_cnt_df['good_cnt'] = good_cnt
                val_cnt_df['good_rate'] = good_rate
                val_cnt_df.insert(0, 'code', fea)
                val_cnt_df.insert(
                    1, 'code_name', code2name.get(fea.upper(), 'N/A'))
                # val_cnt_df['code'] = fea
                # val_cnt_df['code_name'] = code2name.get(fea.upper(), 'N/A')
                if self.results is None:
                    self.results = val_cnt_df
                else:
                    self.results = pd.concat([self.results, val_cnt_df])
                # md.write_title(hn=1, hn_title=fea+':' +
                #                code2name.get(fea.upper(), 'N/A'))
                # # md.write_paragraph(tips_str)
                # md.write_table(val_cnt_df)

    def _qcut(self, df, fea, q):
        intervals, bins = pd.qcut(df[fea], q=q, retbins=True,
                                  precision=4, duplicates='drop')
        val_cnt = pd.value_counts(intervals).sort_index()
        bins = val_cnt.index.tolist()
        cnt = val_cnt.values.tolist()
        val_cnt_df = pd.DataFrame({'bin': bins, 'cnt': cnt})
        bad_rate = []
        good_rate = []
        bad_cnt = []
        good_cnt = []
        for bin in bins:
            bin_val_cnt = df[self.target][df[fea][intervals == bin].index.tolist()
                                          ].value_counts()
            bin_cnt = len(df[fea][intervals == bin])
            bin_bad_cnt, bin_bad_rate, bin_good_cnt, bin_good_rate = self._get_bin_target_dist(
                bin_val_cnt, bin_cnt)
            bad_cnt.append(bin_bad_cnt)
            bad_rate.append(bin_bad_rate)
            good_cnt.append(bin_good_cnt)
            good_rate.append(bin_good_rate)
        val_cnt_df['bad_cnt'] = bad_cnt
        val_cnt_df['bad_rate'] = bad_rate
        val_cnt_df['good_cnt'] = good_cnt
        val_cnt_df['good_rate'] = good_rate
        val_cnt_df.insert(0, 'code', fea)
        val_cnt_df.insert(
            1, 'code_name', code2name.get(fea.upper(), 'N/A'))

        if df[fea].isna().sum() > 0:
            # logger.info('%s 包含空缺值,正在处理空缺值的分布' % fea)
            bin_val_cnt = df[self.target][df[fea].isna()].value_counts()
            bin_cnt = df[fea].isna().sum()
            bin_bad_cnt, bin_bad_rate, bin_good_cnt, bin_good_rate = self._get_bin_target_dist(
                bin_val_cnt, bin_cnt)
            nan_row = [fea, code2name.get(fea.upper(), 'N/A'), 'N/A', bin_cnt, bin_bad_cnt, bin_bad_rate,
                       bin_good_cnt, bin_good_rate]
            # val_cnt_df.append(nan_row)
            val_cnt_df.loc['new'] = nan_row
        if self.results is None:
            self.results = val_cnt_df
        else:
            self.results = pd.concat([self.results, val_cnt_df])

        # md.write_title(hn=1, hn_title=fea+':' +
        #                code2name.get(fea.upper(), 'N/A'))
        # # md.write_paragraph(tips_str)
        # md.write_table(val_cnt_df)

    def _num_binning(self, num_feas):
        logger.info('正在处理数值型特征...')
        for fea in tqdm(num_feas[:]):
            # logger.info(fea)
            self._qcut(self.df, fea, q=self.q)

    def binning(self):
        cat_feas, num_feas = self._cat_num_spliter()
        self._cat_binning(cat_feas)  # 分类型数据探索
        self._num_binning(num_feas)  # 数值型数据探索
        self.results.to_excel(config.output_file, index=False)


if __name__ == "__main__":
    df = pd.read_csv(config.file)
    self = Binning(df, target=config.target)
    self.binning()
